package lego

import (
	"fmt"
	"github.com/gomodule/redigo/redis"
	uuid "github.com/satori/go.uuid"
	"log"
	"time"
)

type Lock struct {
	resource string
	token    string
	//conn     redis.Conn
	GetRedisConn (func() (redis.Conn, error))
	timeout  int
}

func (lock *Lock) tryLock() (ok bool, err error) {
	redisconn, err := lock.GetRedisConn()
	if err != nil {
		return false, err
	}
	defer redisconn.Close()

	_, err = redis.String(redisconn.Do("SET", lock.key(), lock.token, "EX", int(lock.timeout), "NX")) //EX对应的timeout单位是秒
	if err == redis.ErrNil {
		// The lock was not successful, it already exists.
		return false, nil
	}

	if err != nil {
		return false, err
	}
	return true, nil
}

//注意，这里每次轮询的时间是1s，如果这个精度不符合要求，需要调整轮询间隔时间
func (lock *Lock) tryLockWithBlock() (ok bool, err error) {
	redisconn, err := lock.GetRedisConn()
	if err != nil {
		return false, err
	}
	defer redisconn.Close()

	timeout := lock.GetTTL() + 2//lock.timeout + 2 //加多2秒，是为了set nx自动过期之后重试2次

	if timeout > 12 { //最多重试12秒(这个值是估出来的，没有依据，就是不希望重试太长时间)
        timeout = 12
	}

	for timeout > 0 {
		timeout = timeout - 1
		_, err = redis.String(redisconn.Do("SET", lock.key(), lock.token, "EX", int(lock.timeout), "NX")) //EX对应的timeout单位是秒
		if err == redis.ErrNil {
			// The lock was not successful, it already exists.
			time.Sleep(time.Second)
			continue
			//return false, nil
		} else {
			break
		}
	}

	if err == redis.ErrNil {
		// The lock was not successful, it already exists.
		return false, nil
	}

	if err != nil {
		return false, err
	}
	return true, nil
}

func (lock *Lock) Unlock() (err error) {
	redisconn, err := lock.GetRedisConn()
	if err != nil {
		return err
	}
	defer redisconn.Close()

	//TODO 这里应该校验token值，以确认是否有权限unlock
	tokenInRedis, err := redis.String(redisconn.Do("GET", lock.key()))
	/*if err == redis.ErrNil {
	    return nil //不存在这个key
	}*/

	if lock.token != tokenInRedis {
		return fmt.Errorf("redis unlock fail: token not match<%s, %s>", lock.token, tokenInRedis)
	}

	_, err = redisconn.Do("del", lock.key())

	return
}

func (lock *Lock) key() string {
	return fmt.Sprintf("redislock_%s", lock.resource)
}

func (lock *Lock) AddTimeout(ex_time int64) (ok bool, err error) {
	redisconn, err := lock.GetRedisConn()
	if err != nil {
		return false, err
	}
	defer redisconn.Close()


	ttl_time, err := redis.Int64(redisconn.Do("TTL", lock.key()))
	if err != nil {
		log.Fatal("redis get failed:", err)
	}
	if ttl_time > 0 {
		_, err := redis.String(redisconn.Do("SET", lock.key(), lock.token, "EX", int(ttl_time+ex_time)))
		if err == redis.ErrNil {
			return false, nil
		}
		if err != nil {
			return false, err
		}
	}
	return true, nil
}

func TryLock(resource string, /*token string, */) (lock *Lock, ok bool, err error) {
	u2 := uuid.NewV4()
	token := u2.String()
	DefaulTimeout := 8
	return TryLockWithTimeout(resource, token, DefaulTimeout)
}

func TryLockWithTimeout(resource string, token string, timeout int) (lock *Lock, ok bool, err error) {

	lock = &Lock{resource, token, GetRedisConn, timeout}

	ok, err = lock.tryLock()

	if !ok || err != nil {
		lock = nil
	}

	return
}

func TryLockWrapper(GetRedisConn (func() (redis.Conn, error)), resource string, timeout int) (lock *Lock, ok bool, err error) {

	lock, ok, err = TryLock(resource)
	return lock, ok, err
}

func (lock *Lock) GetTTL() int {
	redisconn, err := lock.GetRedisConn()
	ttl_time, err := redis.Int64(redisconn.Do("TTL", lock.key()))
	if err != nil {
		log.Fatal("redis get failed:", err)
	}
	return int(ttl_time)
}